package alg;

import java.util.*;
import java.io.PrintStream;
import data.*;
import static conf.Configuration.*;

/**
 * A course wrapper class - processes the list of courses for use in the algorithm.
 * @author Frans Honig, Maxime Serrano, Raphael Townshend
 */
public class CourseProcessor
{
	/**
	 * The character that stands for a newline.
	 */
	private static final char newLine = '\n';
	
	/**
	 * The students data that will be used globally for statistics monitoring.
	 */
	private ArrayList<Student> globalStudents;
	
	/**
	 * The file name that the  students will be read in from.
	 */
	public final String studentsFileName;
	
	/**
	 * The courses that have been read in thus far.
	 */
	private ArrayList<Course> courses;
	
	/**
	 * Generates a new Course Processor with the given file name.
	 * @param courseFileName The name of the file to use to read courses.
	 * @param studentsFileName The name of the file to use to read students.
	 */
	public CourseProcessor(String courseFileName, String studentsFileName)
	{
		this.studentsFileName = studentsFileName;
		CourseDataReader courseData = new CourseDataReader(courseFileName);
		courseData.read();
		courses = courseData.getAsList();
	}
	
	public Course getCourse(int ID)
	{
		for(Course c: courses)
		{
			if(ID == c.getID())
			{
				return c;
			}
		}
		return null;
	}
	
	/**
	 * Generates a new Course Processor with the default file name.
	 */
	public CourseProcessor(String studentsFileName)
	{
		this.studentsFileName = studentsFileName;
		CourseDataReader courseData = new CourseDataReader();
		courseData.read();
		courses = courseData.getAsList();
	}
	
	/**
	 * Reads in the list of courses from the default file and then returns the generated <code>ArrayList</code>.
	 * @return An ArrayList of all course data.
	 */
	public static ArrayList<Course> generateCourseList()
	{
		CourseDataReader courseData = new CourseDataReader();
		courseData.read();
		return courseData.getAsList();
	}
	
	/**
	 * Prints out the list.
	 * @param courses The list of students to be printed.
	 */
	public static void printList(ArrayList<Course> courses)
	{
		for (int i = 0; i < courses.size(); i++)
		{
			System.out.println(courses.get(i));
		}
	}
	
	/**
	 * Fills all classes' potential students lists with all students
	 * that have said class as a choice.
	 * @param students The list of students read in from data.
	 */
	public void fillClassesWithPotentialStudents(ArrayList<Student> students)
	{
		for (Student s : students)
		{
			for (Course c : courses)
			{
				if(s.getDesirabilityScore(c.getID()) != -1)
				{
					c.addPotentialStudent(s);
				}
			}
		}
		globalStudents = students;
	}
	
	/**
	 * Resolves all classes.
	 */
	public void resolveClasses()
	{		
		for (Course c : courses)
		{
			if (c.isNotFull())
			{
				c.resolve();
			}
		}
	}
	
	/**
	 * Prints out all of the classes (used to generate a solution).
	 * @param out The print stream to print the data to - usually System.out or a file output stream.
	 */
	public void printClasses(PrintStream out)
	{
		for (Course c : courses)
		{
			c.print(out);		
		}
	}
	
	/**
	 * Calculates the heuristic for all courses.
	 * @return The heuristic for all courses.
	 */
	public int calcHeuristic()
	{
		int heuristic = 0;
		ArrayList<Student> students = StudentProcessor.generateStudentList(studentsFileName);
		for (Student s : students)
		{
			if (s.isOnTrip())
			{
				heuristic += s.calcHeuristicScore(1);
			}
		}
		for (Course c : courses)
		{
			heuristic += c.calcHeuristic();
		}
		return heuristic;
	}
	
	/**
	 * Returns courses.
	 * @return A list of all of the read-in courses.
	 */
	public ArrayList<Course> getCourses()
	{
		ArrayList<Course> newCourses = courses;
		return newCourses;
	}
	
	/**
	 * Calculates the number of students per choice number.
	 * @return A string with all of said information in it.
	 */
	public String calcChoices()
	{
		String var = "";
		int firstChoices = 0, secondChoices = 0, thirdChoices = 0, fourthChoices = 0, fifthChoices = 0, sixthChoices = 0;
		int numTrips = 0;
		int specials = 0;
		int forcedSeconds = 0;
		
		for (Student s : globalStudents)
		{
			if (s.isOnTrip())
			{
				numTrips++;
			}
			else if (s.hasSecondChoiceForced())
			{
				secondChoices++;
				forcedSeconds++;
			}
			else
			{
				switch (s.getEnrolledChoiceNumber())
				{
				case -1:
					{
						if (s.isPlaced())
						{
							fifthChoices++;
						}
					}
					break;
				case 0:
					firstChoices++;
					break;
				case 1:
					secondChoices++;
					break;
				case 2:
					thirdChoices++;
					break;
				case 3:
					fourthChoices++;
					break;
				case 4:
					fifthChoices++;
					break;
				case 5:
					sixthChoices++;
					break;
				}
			}
			
		}
		var += "Choice #1: " + firstChoices + newLine;
		var += "Choice #2: " + secondChoices + newLine;
		var += "Choice #3: " + thirdChoices + newLine;
		var += "Choice #4: " + fourthChoices + newLine;
		var += "Choice #5: " + fifthChoices + newLine;
		var += "Choice #6: " + sixthChoices + newLine;
		var += "Trips: " + numTrips + newLine;
		var += "Specials: " + specials + newLine;
		var += "Forced Seconds: " + forcedSeconds + newLine;
		var += "Sum: " + (firstChoices + secondChoices + thirdChoices + fourthChoices + fifthChoices + sixthChoices + numTrips + specials) + newLine;
		return var;
	}
	
	/**
	 * Enrolls all remaining student into classes at random.<br />
	 * The only cases in which this could be necessary is where a student has cheated...
	 */
	public void enrollRemaining()
	{	
		for (Student s : globalStudents)
		{
			if (!(s.isPlaced()) && !(s.isOnTrip()))
			{
				s.forceSecondChoice();
				for (Course c : courses)
				{
					if (c.isNotFull())
					{
						c.tryForceEnroll(s);
					}
				}
			}
		}
	}
	
	/**
	 * Calculates all miscellaneous statistics.
	 * @return A string containing all of said data.
	 */
	public String getSizeAndExceptions()
	{
		String var = "";
		var += "Size: " + globalStudents.size() + newLine;
		int num = 0;
		int numDoubles = 0;
		int placedNum = 0;
		for (Student s : globalStudents)
		{
			if (s != null && !(s.isPlaced()) && !(s.isOnTrip()))
			{
				num++;
			}
			
			if (s != null && (s.isPlaced() || s.isOnTrip()))
			{
				placedNum++;
			}
		}
		var += "# placed: " + placedNum + newLine;
		var += "# unplaced: " + num + newLine;
		var += "# doubles: " + numDoubles + newLine;
		return var;
	}
	
	/**
	 * Prints all students that are not currently enrolled in classes.
	 * @return A string containing all of said data.
	 */
	public String getNonEnrolled()
	{
		String var = "";
		for (Student s : globalStudents)
		{
			if (s != null && !(s.isPlaced()) && !(s.isOnTrip()))
			{
				var += s.toString() + newLine;
			}
		}
		return var;
	}
	
	/**
	 * Cleans up all invalid courses and resolves.
	 */
	public void runCleanup()
	{
		removeInvalidCourses();
		addRejectedStudents();
		resolveAgain();
		
		for (int i = 0; i < NUM_RESOLUTIONS; i++)
		{
			resolveClasses();
		}
	}
	
	/**
	 * Removes all invalid courses.
	 */
	private void removeInvalidCourses()
	{
		ArrayList<Course> coursesToDelete = new ArrayList<Course>();
		for (Course c : courses)
		{
			if(c.shouldBeDeleted())
			{
				coursesToDelete.add(c);
			}
		}
		
		for (Course c : coursesToDelete)
		{
			c.removeCourse(courses);
		}
	}
	
	public void dealWithBadlyPlacedStudents()
	{
		firstRunOfBPSSwaps();
		restOfBPSSwaps();
	}
	
	private void firstRunOfBPSSwaps()
	{
		ArrayList<Student> badStudents = getCurrentBadlyPlacedStudents();
		ArrayList<Student> properStudents = getCurrentProperlyPlacedStudents();
		int iterations = 0;
		while (badStudents.size() > 0 && iterations < 5000)
		{
			for (int i = 0; i < badStudents.size(); i++)
			{
				for(int j = 0; j < properStudents.size(); j++)
				{
				
					Student badStudent = null;
					if (i < badStudents.size())
					{
						badStudent = badStudents.get(i);

					}
					Student goodStudent = properStudents.get(j);
					if(badStudent != null && goodStudent != null)
					{
						Course c1 = getCourse(badStudent.courseEnrolledIn);
						Course c2 = getCourse(goodStudent.courseEnrolledIn);
						if(c1 != null && c2 != null)
						{
							if (badStudent.hasCourseAsChoice(c2) && goodStudent.hasCourseAsChoice(c1))
							{
								AlgorithmTester.swap(badStudent, goodStudent, c1, c2);
								badStudents.remove(badStudent);
								properStudents.add(badStudent);
								badStudent.noLongerHasSecondChoiceForced();
							}
						}
					}
				}
			}
			iterations++;
		}
	}
	
	private void restOfBPSSwaps()
	{
		ArrayList<Student> badStudents = getCurrentBadlyPlacedStudents();	
		ArrayList<Student> properStudents = getCurrentProperlyPlacedStudents();
		int iterations = 0;
		while (badStudents.size() > 0 && iterations < 5000)
		{
			for (int i = 0; i < badStudents.size(); i++)
			{
				for(int j = 0; j < properStudents.size(); j++)
				{
					Student badStudent = null;
					if (i < badStudents.size())
					{
						badStudent = badStudents.get(i);
					}
					Student goodStudent = properStudents.get(j);
					if(badStudent != null && goodStudent != null)
					{
						Course badStudentCourse = getCourse(badStudent.courseEnrolledIn);
						Course currentGoodStudentCourse = getCourse(goodStudent.courseEnrolledIn);
						int[] choices = goodStudent.getChoices();
						for (int choice : choices)
						{
							Course goodStudentCourse = getCourse(choice);
							if(badStudentCourse != null && currentGoodStudentCourse != null && goodStudentCourse != null)
							{
								if (goodStudentCourse.isNotFull() && badStudent.hasCourseAsChoice(currentGoodStudentCourse))
								{
									Student[] studentsInCurrentGoodStudentCourse = currentGoodStudentCourse.studentsEnrolled;
									for (int k = 0; k < studentsInCurrentGoodStudentCourse.length; k++)
									{
										Student s = studentsInCurrentGoodStudentCourse[k];
										if (s != null && s.equals(goodStudent))
										{
											studentsInCurrentGoodStudentCourse[k] = badStudent;
											badStudent.courseEnrolledIn = currentGoodStudentCourse.getID();
											badStudent.position = k;
											badStudent.noLongerHasSecondChoiceForced();
											badStudents.remove(badStudent);
											Student[] studentsInBadCourse = badStudentCourse.studentsEnrolled;
											for(int m = 0; m < studentsInBadCourse.length; m++)
											{
												Student s2 = studentsInBadCourse[m];
												if (s2 != null && s2.equals(badStudent))
												{
													studentsInBadCourse[m] = null;
													break;
												}
											}
											properStudents.add(badStudent);
											break;
										}
									}
									
									for (int n = 0; n < goodStudentCourse.studentsEnrolled.length; n++)
									{
										if (goodStudentCourse.studentsEnrolled[n] == null)
										{
											goodStudentCourse.studentsEnrolled[n] = goodStudent;
											goodStudent.courseEnrolledIn = goodStudentCourse.getID();
											goodStudent.position = n;
											break;
										}
									}
									break;
								}
							}
						}
					}
				}
			}
			iterations++;
		}
	}
	
	private ArrayList<Student> getCurrentBadlyPlacedStudents()
	{
		ArrayList<Student> badlyPlacedStudents = new ArrayList<Student>();
		for (Student s : globalStudents)
		{
			if(s.hasSecondChoiceForced())
			{
				badlyPlacedStudents.add(s);
			}
			/* else if (s.isRandomlyPlaced())
			{
				badlyPlacedStudents.add(s);
			} */
		}
		return badlyPlacedStudents;
	}
	
	private ArrayList<Student> getCurrentProperlyPlacedStudents()
	{
		ArrayList<Student> properlyPlacedStudents = new ArrayList<Student>();
		for (Student s : globalStudents)
		{
			if(!(s.hasSecondChoiceForced()))
			{
				properlyPlacedStudents.add(s);
			}
		}
		return properlyPlacedStudents;
	}
	
	/**
	 * Adds all rejected students to potential lists again.
	 */
	private void addRejectedStudents()
	{
		for (Student s : globalStudents)
		{
			for (Course c : courses)
			{
				if(!(s.isPlaced()) && s.getDesirabilityScore(c.getID()) != -1)
				{
					c.addPotentialStudent(s);
				}
			}
		}
	}
	
	/**
	 * Runs resolve yet again.
	 */
	private void resolveAgain()
	{
		for (Course c : courses)
		{
			if (c.isNotFull())
			{
				c.resolve();
			}
			else
			{
				c.removePotentialStudents();
			}
		}
	}
	
	/**
	 * Forces all teachers to be enrolled.
	 */
	public void enrollTeachers()
	{
		for (Course c : courses)
		{
			c.enrollTeachers();
		}
	}
	
	public void enrollForcedLatePeople()
	{
		for (Course c : courses)
		{
			c.enrollForcedLatePeople();
		}
	}
	
	public ArrayList<Course> getCoursesCopy()
	{
		ArrayList<Course> newCourses = new ArrayList<Course>(courses);
		return newCourses;
	}
	
	public void shuffleCourses()
	{
		Collections.shuffle(courses);
	}
	
	public ArrayList<Student> getGlobalStudents()
	{
		return globalStudents;
	}
}
